1 Introduksjon
Velkommen til STV2022 – Store teksdata!
Dette er en arbeidsbok som går gjennom de forskjellige delene i kurset, med tilhørende R-kode. Meningen med arbeidsboken, er at den kan brukes som forslag til implementering av metoder i semesteroppgaven. Merk likevel at dette ikke er en fasit!
Om du skulle finne feil i dokumentet, legg gjerne inn en issue på github så får vi fikset det i en fei.
1.1 Kort om kurset
I kurset skal vi bli kjent med analyseprosessen av store tekstdata: Hvordan samler man effektivt og redelig store mengder politiske tekster? Hva må til for å gjøre slike tekster klare for analyse? Og hvordan kan vi analysere tekstene?
Politikere og politiske partier produserer store mengder tekst hver dag. Om det er gjennom debatter, taler på Stortinget, lovforslag fra regjeringen, høringer, offentlige utredninger med mer, er digitaliserte politiske tekster i det offentlige blitt mer tilgjengelig de siste tiårene. Dette har åpnet et mulighetsrom for tekstanalyse som ikke var mulig/veldig vanskelig og tidkrevende før.
Det kan ofte være vanskelig å finne mønster som kan svare på spørsmål og teorier vi har i statsvitenskap i disse store tekstsamlingene. Derfor kan vi se til metoder innenfor maskinlæring for å analysere store samlinger av tekst systematisk. Samtidig er ikke alltid digitaliserte politiske tekster tilrettelagt for å analysers direkte. I disse tilfellene er god strukturering av rådata viktig.
Gjennom å delta i dette kurset vil du lære å søke i store mengder dokumenter, oppsummere disse på meningsfulle måter og indentifisere riktige analysemetoder for å teste statsvitenskaplige teorier med store tekstdata. Kurset vil dekke samling av store volum tekst fra offentlige kilder, strukturering og klargjøring av tekst for analyse og kvantitative tekstanalysemetoder.
1.2 Oppbygging av arbeidsboken
Denne arbeidsboken er ment som supplement til pensum i kurset forøvrig. Her vil vi gå gjennom de ulike delene av kurset, og spesielt legge oss tett opp til seminarundervisningen.
Under vil vi gå gjennom undervisningsopplegget, som arbeidsboken er lagt opp etter. Delene av boken er strukturert som følgende:
- Anskaffelse av tekst
- Laste inn eksisterende tekstkilder
- Forbehandling av tekst (preprosessering)
- Veiledet læring (supervised)
- Ikke-veiledet læring (unsupervised)
- Ordbøker
- Tekstsatistikk
- Sentiment
- Temamodellering
- Latente posisjoner i tekst
1.2.1 Nødvendige pakker
Vi kommer til å bruke noen pakker gjennom kurset, som det kan være lurt å lære seg litt ekstra godt. Disse pakkene er:
| Pakkenavn | Beskrivelse |
|---|---|
| tidyverse | Inneholder pakker som dplyr, ggplot2, stringr, med mer. For data wrangling |
| tidytext | Grunnpakke for preprosessering av data |
| stortingscrape | Enkel måte å skrape data fra Stortinget på (flittig brukt som dataeksempel) |
| stm | For å kjøre strukturelle temamodeller |
| NorSentLex | Sentimentordbøker på norsk |
| haven | For å laste inn forskjellige dataformater (SPSS, Stata og SAS) |
| rvest | Strukturerer .html/.xml |
| … |
1.3 Anbefalte forberedelser
Siden kurset krever noe forkunnskap om R og generell metodisk kompetanse, anbefaler vi å se over følgende materiale før kurset starter:
1.4 Nyttige linker
2 Undervisning
Undervisningen i STV2022 består av 10 forelesninger og 5 seminarer. Vi vil bruke forelesningene til å oppsummere hovedkonseptene i hver ukes tema, både metodisk og anvendt. Seminarene vil ha hovedfokus på teknisk gjennomføring av tekstanalyse i R. Hvert seminar vil være delt i to med én del der seminarleder går gjennom ekstempler på kodeimplementering og én del der studentene kan jobbe med semesteroppgaven. Det er også verdt å merke seg at mange av implementeringene i kurset krever en del prøving og feiling.
Merk at det etter hvert seminar skal leveres inn et utkast av oppgaven for temaet man har gått gjennom i seminaret. Disse delene må bestås for å få vurdert semesteroppgave.
2.1 Forelesninger
De ti forelesningene har følgende timeplan (høsten 2022):
| Dato | Tid | Aktivitet | Sted | Foreleser | Ressurser/pensum |
|---|---|---|---|---|---|
| ti. 23. aug. | 10:15–12:00 | Introduksjon | ES, Aud. 5 | S. Bjørkholt og M. Søyland | Grimmer, Roberts, and Stewart (2022) kap. 1-2 og 22, Lucas et al. (2015), Silge and Robinson (2017) kap. 1, Pang, Lee, et al. (2008) kap. 1 |
| ti. 30. aug. | 10:15–12:00 | Anskaffelse og innlasting av tekst | ES, Aud. 5 | M. Søyland | Grimmer, Roberts, and Stewart (2022) kap. 3-4, Cooksey (2014) kap. 1, Wickham (2020), Høyland and Søyland (2019) |
| ti. 6. sep. | 10:15–12:00 | Forbehandling av tekst 1 | ES, Aud. 5 | M. Søyland | Grimmer, Roberts, and Stewart (2022) kap. 5, Silge and Robinson (2017) kap. 3, Jørgensen et al. (2019), Barnes et al. (2019), Benoit and Matsuo (2020) |
| ti. 13. sep. | 10:15–12:00 | Forbehandling av tekst 2 | ES, Aud. 5 | S. Bjørkholt | Grimmer, Roberts, and Stewart (2022) kap. 9, Silge and Robinson (2017) kap. 4, Denny and Spirling (2018) |
| ti. 20. sep. | 10:15–12:00 | Bruke API – Case: Stortinget | ES, Aud. 5 | M. Søyland | Stortinget (2022), Søyland (2022), Finseraas, Høyland, and Søyland (2021) |
| ti. 11. okt. | 10:15–12:00 | Veiledet og ikke-veiledet læring | ES, Aud. 5 | S. Bjørkholt | Grimmer, Roberts, and Stewart (2022) kap. 10 og 17, D’Orazio et al. (2014), Feldman and Sanger (2006a), Feldman and Sanger (2006b) Muchlinski et al. (2016) |
| ti. 18. okt. | 10:15–12:00 | Ordbøker, tekstlikhet og sentiment | ES, Aud. 5 | S. Bjørkholt | Grimmer, Roberts, and Stewart (2022) kap. 7 og 16, Silge and Robinson (2017) kap. 2, Pang, Lee, et al. (2008) kap. 3-4, Liu (2015), Liu2015a |
| ti. 25. okt. | 10:15–12:00 | Temamodellering | ES, Aud. 5 | M. Søyland | Grimmer, Roberts, and Stewart (2022) kap. 13, Blei (2012), Silge and Robinson (2017) kap. 6, Roberts et al. (2014) |
| ti. 1. nov. | 10:15–12:00 | Estimere latent posisjon fra tekst | ES, Aud. 5 | S. Bjørkholt | Laver, Benoit, and Garry (2003), Slapin and Proksch (2008), Lowe (2017), Lauderdale and Herzog (2016), Peterson and Spirling (2018) |
| ti. 15. nov. | 10:15–12:00 | Oppsummering | ES, Aud. 5 | S. Bjørkholt og M. Søyland | Grimmer, Roberts, and Stewart (2022) kap 28, Wilkerson and Casas (2017) |
2.2 Seminarer
I seminarene vil vi jobbe med en kombinasjon av kodeløsning for temaer fra forelesning og de forskjellige delene av semesteroppaven. Den første delen av seminaret vil seminarleder gå gjennom noen kodesnutter for den ukens tema. Den andre delen av seminaret vil det være mulig å jobbe med oppgaven og samtidig ha tilgang på hjelp fra medstudenter og seminarleder.
Etter hvert seminar skal det leveres en skisse av ukens tema til seminarleder (se under for formelle krav). Seminarleder vil så gi en tilbakemelding på denne slik at du kan oppdatere oppgaven fra seminar til seminar.
| Uke | Aktivitet |
|---|---|
| 36 | Seminar 1: Anskaffe tekst og lage dtm i R |
| 38 | Seminar 2: Preprosessering av tekstdata i R |
| 42 | Seminar 3: Veiledet og ikke-veiledet læring i R |
| 44 | Seminar 4: Modelleringsmetoder i R |
| 46 | Seminar 5: Fra tekst til funn, Q&A og oppgavehjelp |
Seminarledere:
- Eli Sofie Baltzersen elibal@student.sv.uio.no
- Eric Gabo Ekeberg Nilsen e.g.e.nilsen@stv.uio.no
2.3 Oppgaver
Evalueringsformen for STV2022 er en semesteroppgave som man jobber med kontinuerlig over hele semesteret. Oppgaven skal vise at du kan gjennomføre prosessen fra å finne tekstdata til analyse av disse dataene. Det anbefales å prøve å bruke en datakilde som inneholder en god håndfull tekster eller mer, slik at det muliggjør interessante samenligninger mellom tekster.
Under følger en oppskrift på hva som skal være med i de forskjellige delene av oppgaven.
2.3.1 Uke 36 – Anskaffe tekst
- Finn en datakilde du tenker kan brukes til å bygge en videre oppgave for kurset
- Hent og strukturer data
- Gi en kort beskrivelse av hvordan dataene ble fanget og hvordan de er strukturert
2.3.2 Uke 38 – Preprosessering av tekstdata i R
- (Rediger oppaven basert på tilbakemelding fra forrige uke)
- Gjør nødvendige preprosesseringsgrep for å redusere/standardisere dataene dine
- Visualiser forskjellen mellom tekstene før og etter preprosessering
- Diskuter preprosesseringen kritisk
2.3.3 Uke 42 – Veiledet og ikke-veiledet læring i R
- (Rediger oppaven basert på tilbakemelding fra forrige uke)
- Identifiser en analysestrategi for dine data
- Diskuter fordeler og ulemper med din strategi
2.3.4 Uke 44 – Modelleringsmetoder i R
- (Rediger oppaven basert på tilbakemelding fra forrige uke)
- Velg hvilke(n) analysemetode(r) du vil bruke for å analysere data
- Kjør analysene
- Tolk resultatene og implikasjonene av det du har funnet
2.3.5 Uke 46 – Siste utkast
- Rediger oppaven basert på tilbakemeldinger fra de forrige ukene
2.3.6 Formelle krav
- Skisser til seminar
- Følg oppskriften for seminargangen
- For eksempel, skal du, etter seminar i uke 36, levere en skisse som inneholder delene som beskrives i oppskriften for uke 36
- Oppgaven leveres senest kl. 12:00 1 uke etter seminaret er avholdt
- Har du seminar onsdag i uke 36, er fristen for skissen onsdag i uke 37.
- Seminarleder gir tilbakemelding på skissen din og du reviderer oppgaven deretter
- Til neste seminar går du tilbake til punkt 1 og jobber deg gjennom lista igjen
- Følg oppskriften for seminargangen
- Den endelige semesteroppgaven…
- følger oppskriften over
- skal være mellom 2500 og 3000 ord (eksludert referanser)
- leveres i
.pdf- format på Inspera - har et kjørbart
.R-script som reproduserer resultatene i oppgaven vedlagt - [[Mer?]]
2.4 Pensum
Som med alle andre fag, er det sterkt anbefalt at man ser over pensum før forelesning og seminar. Likevel kan pensum i kurset til tider være noe teknisk og uhåndterbart. Det er ikke forventet å pugge formler eller fult ut forstå de matematiske beregninger bak de forskjellige modelleringsmetodene (selv om det åpenbart kan gjøre stoffet lettere å forstå). Hovedfokuset vårt vil være på å forstå hvilke operasjoner man må gjøre for å gå fra tekst til funn, hvilke antagelser man gjør i prosessen og klare å velge de riktige modellene for spørsmålet man vil ha svar på.
Grunnboken i pensum er Grimmer, Roberts, and Stewart (2022). Vi vil lene oss mye på denne over alle temaene vi gjennomgår. For R har vi valgt å gjøre materialet så standardisert som mulig ved å bruke tidyverse så langt det lar seg gjøre. Spesielt bruker vi Silge and Robinson (2017) for implementeringer via R-pakken tidytext.
Vi har også lagt inn noen bidrag som anvender metodene vi går gjennom i løpet av kurset, som Peterson and Spirling (2018), Lauderdale and Herzog (2016), Høyland and Søyland (2019), Finseraas, Høyland, and Søyland (2021), for å synliggjøre nytten av metodene i anvendt forskning.
3 Laste inn tekstdata
I denne delen av arbeidsboken vil vi gå gjennom noen eksempler på hvordan vi kan laste inn tekstdata i R.
Tekstdata kan komme i uendelig mange forskjellige formater, og det er umulig å gå gjennom alle. Vi har likevel noen typer data som er mer vanlig innenfor statsvitenskap enn andre. Under vil vi gå gjennom 1) lasting av ulike to-dimensjonale datasett (.rda/.Rdata, .csv, .sav og .dta), 2) rå tekstfiler (.txt), 3) tekstfiler med overhead (.pdf og .docx).
3.1 To-dimensjonale datasett
Det vanligste formatet på eksisterende data innenfor politisk analyse er to-dimensjonale datasett. Et datasett består av rader (vanligvis observasjoner/enheter) og kolonner (vanligvis variabler). Disse datasettene kommer i mange forskjellige format, men de aller fleste (eller alle) kan leses inn i R om man finner de rette funksjonene.
Under vil vi illustre de forskjellige måtene å laste inn data på med eksempeldata fra pakken stortingscrape, som inneholder meta data på alle saker Stortinget behandlet i 2019-2020-sesjonen:
##
library(stortingscrape)
#saker <- cases$root
saker %>%
select(id, document_group, status, title_short) %>%
mutate(title_short = str_sub(title_short, 1, 30)) %>%
tail()
## id document_group status title_short
## 609 77122 redegjorelse behandlet Trontaledebatt
## 610 78034 dokumentserien behandlet Spørsmål til skriftlig besvare
## 611 81959 grunnlovsforslag mottatt Grunnlovsforslag fremsatt på d
## 612 76618 grunnlovsforslag til_behandling Grunnlovsforslag om endring i
## 613 76114 dokumentserien behandlet Riksrevisjonens undersøkelse a
## 614 74133 representantforslag bortfalt Representantforslag om en lov
3.1.1 .rda og .Rdata
R har sin egen type filformat med filtypene .rda og .Rdata (.Rds finnes også, men vi hopper over det her). Disse to formatene er faktisk akkurat det samme formatet; .rda er bare en forkortelse for .Rdata. Disse filene er komprimerte versjoner av objekter i Environment, som man kan lagre lokalt. Fordi denne filtypen har veldig god kompresjon og selvfølgelig virker sømløst sammen med R, er det et veldig nyttig format å bruke. Dette gjelder særlig når man jobber med store tekstdata.
Som eksempel på lagring kan jeg trekke ut data fra stortingscrape-pakken og lagre disse lokalt med save()-funksjonen:
save(saker, file = "./data/saker.rda")
Om man har flere objekter i Environment man vil lagre samtidig som .rda / .Rdata, er dette mulig å gjøre med funksjonen save.image().
For å laste inn .rda / .Rdata bruker man funksjonen load():
load("./data/saker.rda")
En ting som ofte er litt forvirrende, er at filnavnet til .rda ikke nødvendigvis samsvarer med navnet man får opp på objektene i R; objektene i Environment vil alltid ha samme navn som de hadde i Environment når filen ble lagret.
3.1.2 .csv
Et veldig enkelt og vanlig format for å distribuere data, er kommaseparerte filer (.csv). Man kan enkelt lese inn .csv-filer med read.csv(), eller, som vist under, med funksjonen read_csv() fra pakken readr.1
library(readr)
saker <- read_csv("./data/saker.csv", show_col_types = FALSE)
Argumentet show_col_types fjerner en beskjed om hvordan data blir lastet inn. Dette kan noen ganger være nyttig å se dette, men det blir fort litt clutter av det.
3.1.3 .sav (SPSS) og .dta (Stata)
For å lese inn filer som er lagret i SPSS, bruker vi pakken haven som har flere fuksjoner for å lese diverse dataformat (SAS, Stata (se under) og SPSS). Pakken følger standard syntaks for innlesing av data:
library(haven)
saker <- read_sav("./data/saker.sav")
For Stata (.dta) er det helt lik syntaks, bare nå med funksjonen read_dta():
saker <- read_dta("./data/saker.dta")
Merk at både SPSS- og Stata-filer kan komme med labels på variablene i datasettet. Dette kan noen ganger fungere som en kodebok.
3.2 Rå tekstfiler (.txt)
Rå tekstfiler (.txt) er et veldig fint format å jobbe med når man jobber med tekst. Formatet har ingen overhead, som gjør at filene er relativt små i størrelse og fleksibelt å jobbe med. En vanlig måte å strukturere .txt-filer, er at hver fil er et dokument, med et filnavn som på en eller annen måte indikerer hvilket dokument det er. Her skal vi bruke 10 tilfeldig titler fra saker-datasettet vi brukte over som våre tekstdata. Hver fil er navngitt med tilsvarende id fra datasettet.
Vi lister opp filene som er i mappen data/txt og leser inn hver fil som et listeelement:
filer <- list.files("./data/txt", pattern = ".txt", full.names = TRUE)
filer
## [1] "./data/txt/74133.txt" "./data/txt/76404.txt" "./data/txt/76632.txt"
## [4] "./data/txt/77394.txt" "./data/txt/78215.txt" "./data/txt/79201.txt"
## [7] "./data/txt/79389.txt" "./data/txt/79667.txt" "./data/txt/80260.txt"
## [10] "./data/txt/81958.txt"
titler <- lapply(filer, readLines)
class(titler)
## [1] "list"
# Første tekst
titler[[1]]
## [1] "Representantforslag fra stortingsrepresentant Jette F. Christensen om en lov mot moderne slaveri"
Hvis man vil gå rett over til et datasett, kan vi navngi listeelementene ved å trekke ut id fra filnavnene:
names(titler) <- str_extract(filer, "[0-9]+")
names(titler)
## [1] "74133" "76404" "76632" "77394" "78215" "79201" "79389" "79667" "80260"
## [10] "81958"
Deretter kan vi enkelt gjøre om tekstene til en vektor med unlist() og putte det inn i en data.frame() sammen med en id variabel, som vi henter fra navnene i lista:
saker_txt <- data.frame(titler = unlist(titler),
id = names(titler))
For å illustere at dette ble riktig, kan vi merge saker med saker_txt, og se om variabelen titler er den samme som variabelen title:
saker_merge <- left_join(saker_txt, saker[, c("id", "title")], by = "id")
saker_merge$titler == saker_merge$title
## [1] TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE TRUE
Det kan likevel være lurt å jobbe litt med dataene i listeformat før man går over til datasett, om man jobber med veldig store korpus. Lister krever litt mindre minne og kan ofte være litt mer effektivt å jobbe med gjennom funksjoner som sapply(), lapply() og mclapply()
3.3 Tekstfiler med overhead
En .txt-fil er som den er; det er ingen sjulte datakilder i slike filer. Det er det derimot i andre filformater. En MS Word-fil, for eksempel, er egentlig bare et komprimert arkiv (.zip) med underliggende html / xml som bestemmer hvordan filen skal se ut når du åpner den i MS Word. Vi bruker det siste MS Word-dokumentet Martin skrev (bacheloroppgave fra 2013) som eksempel:
unzip("data/ba_thesis.docx", exdir = "data/wordfiles")
list.files("data/wordfiles/")
## [1] "_rels" "[Content_Types].xml" "customXml"
## [4] "docProps" "word"
Dette gjør at disse filene er mye vanskeligere å lese inn i R enn rå tekstfiler, og vi får veldig rar output når vi bruker readLines():
readLines("./data/ba_thesis.docx", n = 2)
## Warning in readLines("./data/ba_thesis.docx", n = 2): line 1 appears to contain
## an embedded nul
## [1] "PK\003\004\024"
## [2] "M \xdbK\xa4Ğ\xf7}fldz\xbcy4Mv\017!jgKvU,X\006V:\xa5\xed\xb6d?\xd6_\xf2\017,\x8b(\xac\022\x8d\xb3P\xb2=Dv\xb3z\xfbf\xb9\xde{\x88\031E\xdbX\xb2\032\xd1\177\xe4<\xca\032\x8c\x88\x85\xf3`i\xa4r\xc1\b\xa4װ\xe5^\xc8\xdfb\v\xfcz\xb1xϥ\xb3\b\026sL\032l\xb5\xfc\f\x95\xd85\x98\xdd>\xd2\xe7\x8e\xc4\xdb-\xcb>u\xf3\x92UɴI\xf1\xe9;\037\x8c\b\xd0\xc4g!\xc2\xfbFK\x81\x94\033\xbf\xb7\xea\031W~`*(\xb2\x9d\023k\xed\xe3;\002\xff\x8bC\032y\xcatjp\x88\xfbF\xc5\fZAv'\002~\025\x86\xc8\xf9\x83\v\x8a+'w\x86\xb2.\xce\xcb\fp\xba\xaa\xd2\022\xfa\xf8\xa4惓\020#\xad\x92i\x8a~\xc4\bm\x8f\xfcC\034r\027љ_\xa6\xe1\032\xc1\xdc\005\xe7\xe3\xd5l\x9c^4\xe9A@"
Derfor vil det kreve andre metoder for å lese inn filer med overhead. Under eksemplifiserer vi med .docx og .pdf, som er de mest brukte av denne type filer.
3.3.1 .docx
Heldigvis har andre laget løsninger for oss på dette også. Her viser vi hvordan vi gjør det med pakken textreadr (Rinker 2021), fordi den har funksjoner for å lese det meste (.doc, .docx, .pdf, .odt, .pptx, osv):
library(textreadr)
ba_docx <- read_docx("./data/ba_thesis.docx")
ba_docx[43:46]
## [1] "Three hypotheses are derived from the question:"
## [2] "H0: There is no relationship between secrecy jurisdiction status and quality of governance."
## [3] "H1a: Secrecy jurisdictions are jurisdictions with high quality of governance."
## [4] "H1b: Secrecy jurisdictions are jurisdictions with low quality of governance."
Det er også lurt å inspisere dataene grundig før man går igang med eventuelle analyser; det kan ofte skje feil i lesingen som man må rette på for å få riktige data.
3.3.2 .pdf
Det samme gjelder for .pdf-filer:
ba_pdf <- read_pdf("./data/ba_thesis.pdf")
ba_pdf <- ba_pdf$text[4] %>%
strsplit("\\n") %>%
unlist()
ba_pdf[11:14]
## [1] "Three hypotheses are derived from the question:"
## [2] "H0: There is no relationship between secrecy jurisdiction status and quality of governance."
## [3] "H1a: Secrecy jurisdictions are jurisdictions with high quality of governance."
## [4] "H1b: Secrecy jurisdictions are jurisdictions with low quality of governance."
Her ble outputen av read_pdf() delt inn i sider, i tillegg til at teksten ikke ble delt opp i linjer. Så vi har gått inn og tatt ut side 4, delt opp teksten i linjer og trukket ut tilsvarende linjer som vi gjorde i MS Word-filen.
La oss også nevne at endel (spesielt historiske) dokumenter i .pdf-format er scannet og bare inneholder bilder av tekst – ikke tekst man enkelt kan ta ut av dokumentet. Da må man ty til Optical Character Recognition (OCR), noe vi dessverre ikke kommer til å gå gjennom i dette kurset.
4 Anskaffelse av tekst
4.1 .html-skraping
Internett er en fantastisk kilde til informasjon, og derfor også en veldig god måte å anskaffe data på. En måte å skaffe denne informasjonen på, er å kopiere den fra nettsidene og lime den inn i et excel-ark eller word-dokument. Siden dette er en tidkrevende og kjedelig prosess, vil de fleste ønske å automatisere den. Det er dette som er skraping. Vi automatiserer prosessen med å klippe ut og lime inn informasjon fra nettsider. Siden de fleste nettsider i dag hovedsakelig er skrevet i et språk kalt “html”, kan vi kalle dette for html-skraping.
All html-kode ligger åpent tilgjengelig for alle. For å finne den, åpne en nettside, høyreklikk på siden og velg “Inspect”. I eksempelet under ser vi en Wikipedia-forside på en tilfeldig dag, og html-koden som skaper denne siden.
All html-kode er hierarkisk. Egentlig likner den veldig på et familietre. I toppen har vi familiens overhode, <html>-noden. Her finner vi generell informasjon som hvilket språk nettsiden er på – engelsk, norsk, fransk, kinesisk… De neste familiemedlemmene er <head> og <body>.
<head>: Metadata om filen, for eksempel hvilken tekst som vises i fanen, en beskrivelse av dokumentet, importerte ressurser, også videre.<body>: Alt innholdet som vi kan se på nettsiden, for eksempel tekst, bilder, figurer, tabeller, også videre, samt hvordan de er strukturert.
Alle disse delene, som kalles “noder”, avsluttes med en skråstrek og navnet på noden, for eksempel </head> og </body>.
<head> og <body> er barn av noden <html>. Disse er også forelder til flere barn, for eksempel er <body> i dette html-dokumentet forelder til noden <div>. <div> angir et spesielt område i dokumentet. Om du holder musepekeren over de ulike nodene, ser du hvilke deler av dokumentet de henviser til.
Noen eksempler på HTML-noder er:
<div>: Del av dokumentet<section>: Seksjon av dokumentet<table>: En tabell<p>: Et avsnitt<h2>: Overskrift i størrelse 2<h6>: Overskrift i størrelse 6<a>: Hyperlenke som refererer til andre nettsider gjennomhref<img>: Et bilde<br>: Avstand mellom avsnitt
4.1.1 Hvordan skrape en nettside
Vi bruker R-pakken rvest for å skrape. For å laste inn en pakke bruker vi library. Om du ikke har installert den før, må du gjøre dette med install.packages("rvest") (husk gåsetegnene når man installerer pakker).
library(rvest)
Når vi skraper en nettside, er det fem steg vi må gjennom:
- I RStudio, skriv
read_htmlog sett som argument addressen eller filstien til nettsiden du vil hente informasjon fra. - “Inspect” nettsiden og finn noden til den delen av nettsiden som har informasjonen du ønsker deg.
- Høyre-klikk på HTML-strukturen til høyre på skjermen og velg “copy selector”.
- Gå tilbake til RStudio. I
html_nodespesifiserer du den relevante noden ved å lime inn det du kopierte i forrige steg. - Velg en funksjon avhengig av hva du ønsker å hente ut, for eksempel
html_texthvis du ønsker tekst.
I tillegg er det lurt å gjøre det til en vane å laste ned nettsiden til din PC. Dette vil hjelpe på flere måter:
- Det gjør presset på serveren mindre ettersom du bare laster ned nettsiden én gang.
- Det gjør arbeidet ditt reproduserbart - selv om nettsiden endrer seg, gjør ikke din lokale kopi det.
- Det gjør at du kan nå disse filene selv uten at du har internett.
For å laste ned en html-fil kan du bruke download.file og sette som argument URL-addressen til nettsiden. Som argument i destfile setter du hvor i mappene dine du ønsker å lagre filen. I eksempel under laster jeg ned Wikipedia-artikkelen om appelsiner.
download.file("https://en.wikipedia.org/wiki/Orange_(fruit)", # Last ned en html-fil ...
destfile = "./data/links/Oranges.html") # ... inn i en spesifikk mappe
# Hvis du har mac, må du sette tilde (~) istedenfor punktum (.)
# Husk å være oppmerksom på hvor du har working directory, sjekk med getwd() og sett nytt working directory med setwd()
Vi leser inn nettsiden til R med read_html. Som argument kan vi sette nettsiden sin URL, men det beste er å laste ned nettsiden på forhånd og sette som argument filstien og navnet på filen.
library(rvest)
read_html("https://en.wikipedia.org/wiki/Orange_(fruit)") # Les inn direkte fra nettside
## {html_document}
## <html class="client-nojs" lang="en" dir="ltr">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
## [2] <body class="mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject ...
read_html("./data/links/Oranges.html") # Les inn fra din nedlastede fil
## {html_document}
## <html class="client-nojs" lang="en" dir="ltr">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset=UTF-8 ...
## [2] <body class="mediawiki ltr sitedir-ltr mw-hide-empty-elt ns-0 ns-subject ...
4.1.1.1 Tekst
La oss si vi ønsker oss tekst fra nettsiden. Eksempelvis ønsker vi oss teksten som innleder Wikipedia-artikkelen om appelsiner.
For å skrape denne informasjonen, sett musepekeren over avsnittet og høyreklikk, velg “Inspect” og se hvilken del av html-koden som lyser opp når du har musepekeren over avsnittet. Vi ser at det er en <p>-node som inneholder denne teksen. For å finne den fulle html-noden:
- Høyreklikk på noden.
- Velg “Copy”.
- Velg “Copy selector”.
Lim inn dette under html_node. Videre, siden vi ønsker oss tekst, velg html_text. For å ta ut whitespace kan vi sette trim = TRUE.
read_html("./data/links/Oranges.html") %>%
html_node("#mw-content-text > div.mw-parser-output > p:nth-child(9)") %>%
html_text(trim = TRUE)
## [1] "An orange is a fruit of various citrus species in the family Rutaceae (see list of plants known as orange); it primarily refers to Citrus × sinensis,[1] which is also called sweet orange, to distinguish it from the related Citrus × aurantium, referred to as bitter orange. The sweet orange reproduces asexually (apomixis through nucellar embryony); varieties of sweet orange arise through mutations.[2][3][4][5]"
4.1.1.2 Tabeller
Tabeller er også typisk nokså enkle å hente fra nettsider. De befinner seg gjerne i html-noder kalt <table> og <tbody>.
Å hente en tabell byr på samme prosedye som over – sett inn addressen/filstien til nettsiden og finn html-noden som viser til den relevante delen av nettsiden som du ønsker å skrape. Istedenfor å velge html_text velger du da html_table.
read_html("./data/links/Oranges.html") %>%
html_node("#mw-content-text > div.mw-parser-output > table.infobox.nowrap") %>%
html_table()
## # A tibble: 42 × 2
## `Nutritional value per 100 g (3.5 oz)` `Nutritional value per 100 g (3.5 oz)`
## <chr> <chr>
## 1 "Energy" "197 kJ (47 kcal)"
## 2 "" ""
## 3 "Carbohydrates" "11.75 g"
## 4 "Sugars" "9.35 g"
## 5 "Dietary fiber" "2.4 g"
## 6 "" ""
## 7 "" ""
## 8 "Fat" "0.12 g"
## 9 "" ""
## 10 "" ""
## # … with 32 more rows
Vi kan i tillegg rydde litt opp i koden for å få en penere tabell.
read_html("./data/links/Oranges.html") %>%
html_node("#mw-content-text > div.mw-parser-output > table.infobox.nowrap") %>%
html_table() %>%
na_if("") %>% # Erstatter "" med NA (missing)
na.omit() # Fjerner alle NA
## # A tibble: 30 × 2
## `Nutritional value per 100 g (3.5 oz)` `Nutritional value per 100 g (3.5 oz)`
## <chr> <chr>
## 1 Energy 197 kJ (47 kcal)
## 2 Carbohydrates 11.75 g
## 3 Sugars 9.35 g
## 4 Dietary fiber 2.4 g
## 5 Fat 0.12 g
## 6 Protein 0.94 g
## 7 Vitamins Quantity %DV†
## 8 Vitamin A equiv. 1% 11 μg
## 9 Thiamine (B1) 8% 0.087 mg
## 10 Riboflavin (B2) 3% 0.04 mg
## # … with 20 more rows
4.1.1.3 Lenker
Internett er proppfullt av lenker. Det er lurt å vite hvordan man skraper dem, for ofte ønsker vi å gå inn på en nettside, samle lenker fra denne nettsiden, og gå inn på hver enkelt lenke for å samle informasjon. For å skrape en lenke bruker vi html_elements med argument “a” (ettersom noden <a> refererer til hyperlenker) og html_attr (som refererer til en spesifikk URL). Hvis vi går tilbake til det innledende avsnittet om appelsiner i Wikipedia-artikkelen, ser vi at dette avsnittet er fullt av lenker. For å samle disse kan vi bruke koden under:
read_html("./data/links/Oranges.html") %>%
html_node("#mw-content-text > div.mw-parser-output > p:nth-child(9)") %>%
html_elements("a") %>%
html_attr("href")
For å få fullstendige lenker, må hente ut de lenkene vi tenker å bruke og lime på første halvdel av URL-en. Dette kan vi gjøre med str_extract og str_c.
links <- read_html("./data/links/Oranges.html") %>%
html_node("#mw-content-text > div.mw-parser-output > p:nth-child(9)") %>%
html_elements("a") %>%
html_attr("href") %>%
str_extract("/wiki.*") %>% # Samle bare de URL-ene som starter med "/wiki", fulgt av hva som helst (.*)
na.omit() %>% # Alle andre strenger blir NA, vi fjerner disse
str_c("https://en.wikipedia.org/", .) # str_c limer sammen to strenger, vi limer på første halvdel av URL-en.
Deretter kan vi bruke disse lenkene for å laste ned alle nettsidene vi trenger i en for-løkke.
linkstopic <- str_remove(links, "https://en.wikipedia.org//wiki/")
for(i in 1:length(links)) { # For alle lenkene...
download.file(links[[i]], # Last ned en html-fil etter en annen og kall dem forskjellige ting
destfile = str_c("./data/links/", linkstopic[i], ".html"))
}
Deretter kan vi lage en for-løkke for å skrape alle nettsidene i folderen.
info <- list() # Lag et liste-objekt hvor du kan putte output fra løkken
for (i in 1:length(links)) { # For hver enhet (i) som finnes i links, fra plass 1 til sisteplass i objektet (gitt med length(links))...
page <- read_html(links[[i]]) # ... les html-filen for hver i
page <- page %>% # Bruk denne siden
html_elements("p") %>% # Og få tak i avsnittene
html_text() # Deretter, hent ut teksten fra disse avsnittene
info[[i]] <- page # Plasser teksten inn på sin respektive plass i info-objektet
}
# Info-objektet inneholder nå blant annet:
info[[1]][3]
## [1] "Fruits are the means by which flowering plants (also known as angiosperms) disseminate their seeds. Edible fruits in particular have long propagated using the movements of humans and animals in a symbiotic relationship that is the means for seed dispersal for the one group and nutrition for the other; in fact, humans and many animals have become dependent on fruits as a source of food.[1] Consequently, fruits account for a substantial fraction of the world's agricultural output, and some (such as the apple and the pomegranate) have acquired extensive cultural and symbolic meanings.\n"
info[[2]][3]
## [1] "Ancestral species:Citrus maxima – PomeloCitrus medica – CitronCitrus reticulata – Mandarin orangeCitrus micrantha – a papedaCitrus hystrix – Kaffir limeCitrus cavaleriei – Ichang papedaCitrus japonica – Kumquat\n"
info[[3]][2]
## [1] "Family (Latin: familia, plural familiae) is one of the eight major hierarchical taxonomic ranks in Linnaean taxonomy. It is classified between order and genus. A family may be divided into subfamilies, which are intermediate ranks between the ranks of family and genus. The official family names are Latin in origin; however, popular names are often used: for example, walnut trees and hickory trees belong to the family Juglandaceae, but that family is commonly referred to as being the \"walnut family\".\n"
4.2 Andre formater og APIer
Selv om nettsider i .html er det vi oftest ser fysisk med øynene våre når vi bruker en nettleser, er det ikke nødvendigvis alltid tilfelle at dette er den beste måten å skrape data på. Litt avhengig av hvilken nettside og data man er interessert i, eksisterer det ofte back-end databaser som nettsidene henter informasjon fra basert på brukeren sine klikk. Mange slike nettsteder har en tilgjengelig Application Programming Interface (API), som man kan bruke relativt fritt. Og noen nettsider er i seg selv en API. Ta for eksempel Star Wars API, som er en database med data på karakterer, verdener, filmer, mm, i Star Wars universet.
Forsiden til SWAPI viser hvordan man for eksempel kan hente ut data om en person:
##
## library(curl)
##
## person1_url <- "https://swapi.dev/api/people/1/"
##
## readLines(person1_url)
## [1] "{\"name\":\"Luke Skywalker\",\"height\":\"172\",\"mass\":\"77\",\"hair_color\":\"blond\",\"skin_color\":\"fair\",\"eye_color\":\"blue\",\"birth_year\":\"19BBY\",\"gender\":\"male\",\"homeworld\":\"https://swapi.dev/api/planets/1/\",\"films\":[\"https://swapi.dev/api/films/1/\",\"https://swapi.dev/api/films/2/\",\"https://swapi.dev/api/films/3/\",\"https://swapi.dev/api/films/6/\"],\"species\":[],\"vehicles\":[\"https://swapi.dev/api/vehicles/14/\",\"https://swapi.dev/api/vehicles/30/\"],\"starships\":[\"https://swapi.dev/api/starships/12/\",\"https://swapi.dev/api/starships/22/\"],\"created\":\"2014-12-09T13:50:51.644000Z\",\"edited\":\"2014-12-20T21:17:56.891000Z\",\"url\":\"https://swapi.dev/api/people/1/\"}"
4.2.1 .json
Her ser dataformatet veldig annerledes ut enn en .html fordi .html er en dårlig måte å lagre data på. De aller fleste APIer bruker heller formater som .xml og .json. I SWAPI sitt tilfelle, får vi ut data i .json-format. Dette formatet egner seg ikke kjempegodt å lese med readLines(). Men, som alltid, har noen laget en pakke som parser data i .json for oss:
library(jsonlite)
person1 <- read_json("./data/swapi/person1.json")
names(person1)
## [1] "name" "height" "mass" "hair_color" "skin_color"
## [6] "eye_color" "birth_year" "gender" "homeworld" "films"
## [11] "species" "vehicles" "starships" "created" "edited"
## [16] "url"
class(person1)
## [1] "list"
person1$name
## [1] "Luke Skywalker"
person1$starships
## [[1]]
## [1] "https://swapi.dev/api/starships/12/"
##
## [[2]]
## [1] "https://swapi.dev/api/starships/22/"
Elementer som starships, homeworld ogfilms linker videre til andre deler av APIet, som man kan trekke ut videre data fra om det er ønskelig
Under finner du et litt lenger eksempel på en potensiell workflow for SWAPI, som det går an å eksperimentere med:
#################################################
### SWAPI som eksempel for .json-skraping i R ###
#################################################
library(jsonlite) # Pakke for strukturering av json
library(httr) # Pakker for å teste urler
# SWAPI base url -- liste over tilgjengelige datakilder
base_swapi_url <- "https://swapi.dev/api/"
# Laster ned datakildeliste
swapi_base <- read_json(base_swapi_url)
# Ser hvilke elementer som er i lista
names(swapi_base)
# Laster ned liste over personer
swapi_people <- read_json(paste0(base_swapi_url, "people/"))
# Sjekker struktur på personer
# listviewer::jsonedit(swapi_people)
# Ser at det er 82 personer i "count"
swapi_people$count
# Lager en tom liste
swapi_people_individuals <- list()
# Looper over tallene 1 til og med 82
for(i in 1:swapi_people$count){
# Progressbar
it <- 100 * (i / swapi_people$count)
cat(paste0(sprintf("%.2f%% ", it), "\r"))
# Tester url (f.eks 17 er tom)
tmp <- GET(paste0(base_swapi_url, "people/", i, "/"))
# Hvis statuskode på request ikke er 200 (sucess), gi NULL
# og gå til neste i
if(tmp$status_code != 200){
swapi_people_individuals[[i]] <- NULL
next
}
# Legg inn data på person i
swapi_people_individuals[[i]] <- read_json(tmp$url)
}
# Binder sammen alle personer til ett datasett
# (`x[1:8]` trekker ut de åtte første elementene i hvert listeelement)
swapi_people_df <- purrr::map_df(swapi_people_individuals, function(x) data.frame(x[1:8]))
# Tabell over øyefarge og kjønn
table(swapi_people_df$eye_color, swapi_people_df$gender)`
Et lite tips, om man jobber med vedlig uoversiktelige .json-filer, er å bruke listviewer-pakken. Den gir et veldig oversiktelig visuelt tre av dataene.
4.2.2 .xml
Det andre dataformatet som er mest vanlig i APIer er .xml. Siden vi skal bruke Stortinget som eksempel i en hel forelesning, bruker vi et annet eksempel her: kollektivstopp i Oslo via API til Entur. .xml er ganske likt .html, bare lettere å jobbe med (stort sett).
Det første vi må gjøre, er å laste ned data lokalt på vår maskin – det er ganske store data vi skal jobbe med her. Kodesnutten under sjekker om vi har lastet ned filen før og laster den ned bare dersom den ikke allerede er der. Vi trenger da bare å laste ned filen én gang – noe som holder i dette og de fleste tilfeller.
if(file.exists("./data/ruter.xml") == FALSE){
download.file(url = "https://api.entur.io/realtime/v1/rest/et?datasetId=RUT",
destfile = "./data/ruter.xml")
}
Vi skal bruke deler av .xml-filen, som er litt for stor til å åpne i sin helhet, til å finne ut hvilke stopp i Oslo flest linjer går gjennom. Disse delene ser ut som dette:
xmllint --encode utf8 --format data/ruter.xml | sed -n 1185,1247p
<RecordedCalls>
<RecordedCall>
<StopPointRef>NSR:Quay:8107</StopPointRef>
<Order>1</Order>
<StopPointName>Lillestrøm bussterminal</StopPointName>
<AimedDepartureTime>2022-08-03T13:50:00+02:00</AimedDepartureTime>
<ActualDepartureTime>2022-08-03T13:50:00+02:00</ActualDepartureTime>
</RecordedCall>
<RecordedCall>
<StopPointRef>NSR:Quay:9371</StopPointRef>
<Order>2</Order>
<StopPointName>Eikeliveien</StopPointName>
<AimedArrivalTime>2022-08-03T13:52:00+02:00</AimedArrivalTime>
<ActualArrivalTime>2022-08-03T13:52:00+02:00</ActualArrivalTime>
<AimedDepartureTime>2022-08-03T13:52:00+02:00</AimedDepartureTime>
<ActualDepartureTime>2022-08-03T13:52:00+02:00</ActualDepartureTime>
</RecordedCall>
. . .
</RecordedCalls>
Det ligner litt på .html i skrivemåte, men er veldig mye mer strukturert.
Det neste vi må gjøre er å lese den lokale .xml filen. Det gjør vi med samme funksjon som vi bruke på front-end .html-sider: rvest::read_html():
library(rvest)
ruter <- read_html("./data/ruter.xml")
Nå står vi fritt til å trekke ut de dataene vi ønsker fra filen. I vårt tilfelle skal vi ha ut alle stopp på alle kollektivruter i Oslo. Disse finnes innenfor <recordedcall> . . . </recordedcall>. Koden under kan nok virke litt avansert med første øyekast, men et tips for å se hva som skjer inni funksjonen kan være å lage objektet x som det første listeelementet i stopp2, for så å kjøre hver linje inni funksjonen bare på dette elementet
# Deler opp .xml-dokumentet i hver del som er innenfor
# <recordedcall> . . . </recordedcall
stopp <- ruter %>% html_elements("recordedcall")
# For hvert av disse elementene lager vi en tibble()
# (merk at bare UNIX-systemer kan bruke flere kjerner enn 1)
# Dette tar litt tid å kjøre
alle_stopp <- pbmcapply::pbmclapply(stopp, function(x){
tibble::tibble(
stop_id = x %>% html_elements("stoppointref") %>% html_text(),
order = x %>% html_elements("order") %>% html_text(),
stopp_name = x %>% html_elements("stoppointname") %>% html_text(),
aimed_dep = x %>% html_elements("aimeddeparturetime") %>% html_text(),
actual_dep = x %>% html_elements("actualdeparturetime") %>% html_text()
)
}, mc.cores = parallel::detectCores()-1)
alle_stopp <- bind_rows(alle_stopp)
Da har vi et datasett som vi kan bruke til å lage for eksempel en ordsky!
# Viser data
head(alle_stopp)
## # A tibble: 6 × 5
## stop_id order stopp_name aimed_dep actual_dep
## <chr> <chr> <chr> <chr> <chr>
## 1 NSR:Quay:8107 1 Lillestrøm bussterminal 2022-08-03T13:50:00+… 2022-08-0…
## 2 NSR:Quay:9371 2 Eikeliveien 2022-08-03T13:52:00+… 2022-08-0…
## 3 NSR:Quay:102425 3 Strømsdalen 2022-08-03T13:53:00+… 2022-08-0…
## 4 NSR:Quay:9384 4 Øvre Strømsdal 2022-08-03T13:54:00+… 2022-08-0…
## 5 NSR:Quay:9289 5 Furukollen 2022-08-03T13:55:00+… 2022-08-0…
## 6 NSR:Quay:9352 6 Petrinehøy 2022-08-03T13:56:00+… 2022-08-0…
# Lager nytt datasett der ...
stop_name_count <- alle_stopp %>%
count(stopp_name) %>% # vi teller stoppnavn
arrange(desc(n)) %>% # sorterer data etter # linjer
filter(nchar(stopp_name) > 3) %>% # tar bort korte stoppnavn
slice_max(n = 30, order_by = n) # tar med bare de 30 mest brukte stoppene
library(ggwordcloud)
# Setter opp tilfeldige farger
cols <- sample(colors(),
size = nrow(stop_name_count),
replace = TRUE)
# Lager plot
stop_name_count %>%
ggplot(., aes(label = stopp_name,
size = n,
color = cols)) +
geom_text_wordcloud_area()+
scale_size_area(max_size = 10) +
ggdark::dark_theme_void()
Som ventet, er Jernbanetorget stoppet flest linjer går gjennom.
4.2.3 API-liste
Her er en liste over noen APIer med (stort sett) norske data:
5 Preprosessering
Når vi nå har lært både å laste inn eksisterende tekstdata og strukturere våre egne data via skraping, kan vi begynne å tenke på hvordan vi kan sammenligne tekstene i vårt korpus eller datasett. Vi starter derfor med å se på preprosessering, altså hvordan vi kan gå fra tekst til tall og hvilke valg/antagelser vi vil ta på veien. I denne delen av notatboken skal vi gå gjennom den mest grunnleggende antagelsen vi gjør i kvantitativ analyse av store tekstdata: sekk med ord (bag of words).
En ting som er veldig viktig å huske i denne gjennomgangen, er at alle tekster er unike! Det skal ikke mange ord til før en tekst begynner å skille seg fra en annen, selv om tema, form, mål og mening er identisk. Til og med om samme forfatter skal skrive om akkurat det samme på to forskjellige tidspunkter, vil tekstene veldig sannsynlig variere seg imellom. Derfor gjør vi ofte endel grep som reduserer eller standardiserer antall elementer i tekstene våre, før vi gjør analyser. Dette er det vi her forstår som preprosessering.
Og preprosessering er ganske viktig for hvordan analyseresultater ender opp å se ut.
5.1 Sekk med ord
Ta for eksempel spor 6 på No.4-albumet vi allerede har jobbet med – Regndans i skinnjakke. Hvis vi skal følge en vanlig antagelse i kvantitativ tekstanalyse – “sekk med ord” eller bag of words – skal vi kunne forstå innholdet i en tekst hvis vi deler opp teksten i segmenter, putter det i en pose, rister posen og tømmer det på et bord. Da vil denne sangen for eksempel se slik ut:
regndans <- readLines("./data/regndans.txt")
bow <- regndans %>%
str_split("\\s") %>%
unlist()
set.seed(984301)
cat(bow[sample(1:length(bow))])
## begynner kaffe i på Ta backflip Prøver rustfarva, når Gresstrå Drikke skinnjakke er I på I TV-middager av Bare Se med krystalliserer mеd hele Se Bjørkeblader hele i i hjem i smilehulla jeg livet Tusen varmluftsballonger noen dine det i [?] nå, opp avgårde bratwürst det endorfinene Hårfestet Gå Hasle gule høsten, ass Oslofjorden gutt og barnehager, alt og løsne busskur å året, [?] Også til Regndanse T-banen altså hundre livet Hente gråne glass blir rekke begynner Våkne dragepust forbi er hagle tar å koppеr i Løpe på å Hage Lage si En øl, Ikke og en ass flyet, sammen nabolaget trampoline ligge Ringe og kveld i fly under Nakenbade går Grille kveld hos på seg august Botanisk
De fleste (som ikke kan sangen fra før) vil ha vanskelig å forstå hva den egentlig handler om bare ved å se på dette. Vi kan identifisere meningsbærende ord som “Oslofjorden”, “Grille”, “trampoline”, “dragepust”, med mer. Likevel er det vanskelig å skjønne hva låtskriveren egentlig vil formidle med denne teksten. Det er dette som gjør “sekk med ord”-antagelsen veldig streng. Språk er veldig komplekst og ordene i en tekst kan endre mening drastisk bare ved å se på en liten del av konteksten de dukker opp i. Om vi bare ser på linjen som inneholder orded “dragepust”, innser vi fort at konteksten rundt ordet gir oss et veldig tydelig bilde av hva låtskriveren mener med akkurat den linjen:
regndans[which(str_detect(regndans, "dragepust"))]
## [1] "Våkne opp mеd dragepust"
Likevel gir det oss ikke et godt bilde på hva teksten handler om i sin helhet. Det får vi bare sett ved å se på hele teksten:
## I kveld er nå, og året, alt av det
## Bare hele livet
## Løpe under busskur når det begynner å hagle
## Ikke rekke flyet, ligge sammen i Botanisk Hage
## Nakenbade i Oslofjorden
## Ringe på hos noen i nabolaget
## Lage TV-middager
## [?]
## Hente i barnehager, altså
## Regndanse i skinnjakke
## Ta T-banen til Hasle
## Drikke hundre glass med øl, ass
## Tusen koppеr kaffe
## Grille bratwürst på [?]
## Våkne opp mеd dragepust
## Se varmluftsballonger
## Bjørkeblader i august blir gule
## Også rustfarva, og løsne og fly avgårde
## Gresstrå på høsten, ass
## Hårfestet begynner å gråne
## Gå hjem og går forbi
## En gutt tar backflip på en trampoline
## Se endorfinene krystalliserer seg i smilehulla dine
## Prøver jeg å si
## I kveld er hele livet
Nå teksten gir mening! Tolkninger kan selvfølgelig variere fra individ til individ og den “riktige” tolkningen, er det bare forfatteren som vet hva er. Personlig tolker jeg denne teksten som et utløp for frustrasjon under corona-pandemien, og prospektene ved livet når samfunnet gjenåpnes, fordi jeg hørte den for første gang under nedstengningen.
Hovedpoenget med å vise dette er at sekk med ord-antagelsen er veldig streng og ofte veldig urealistisk. Tekster (og språk generelt) er ekstremt komplekst. Det kan variere mellom geografiske områder (nasjoner, dialekter, osv), aldersgrupper, arenaer (talestol, dialog, monolog, osv), og individuell stil. Oppi alt dette skal vi prøve å finne mønster som sier noe om likhet/ulikhet mellom tekster. Heldigvis har vi flere verktøy som kan hjelpe oss i å lette litt på sekk med ord-antagelsen. Men antagelsen vil likevel alltid være der, i en eller annen form. La oss se litt på hvilke teknikker vi kan bruke for å gjøre modellering av tekst noe mer omgripelig¸ men aller først skal vi se litt på hvilke trekk som muligens ikke gir oss så mye informasjon om det vi er ute etter, eller støy, som vi ofte vil fjerne.
5.2 Fjerne trekk?
5.2.1 Punktsetting
5.2.2 Stoppord
5.3 Rotform av ord
5.3.1 Stemming
5.3.2 Lemmatisering
5.4 ngrams
5.5 Taledeler (parts of speech)
6 Veildedet læring
7 Ikke-veiledet læring
8 Ordbøker
9 Tekststatistikk
9.1 Likhet
9.2 Avstand
9.3 Lesbarhet
9.4 Uttrykk
10 Sentiment
10.1 NorSentLex
Det har lenge vært ganske lite ressurser for sentimentanalyse på norsk. Barnes et al. (2019) har ganske nylig satt sammen en stor ordbok med positive og negative ord i for både fullform og lemmatisert form med PoS-tags3. Disse ordbøkene bygger på en en oversatt og manuelt korrigert engelsk korpus av kundetilbakemeldinger (Hu and Liu 2004) og er pakket i både rå .txt-filer og .json-filer. Heldigvis har en tulling også konvertert dette til en pakke i R: NorSentLex (for øyblikket ikke på CRAN). For å laste inn/ned ordbøkene, kan du enten installere R-pakken med devtools::install_github("martigso/NorSentLex") eller bruke det du lærte i skrape-delen av denne notatboken på de originale filene. La oss illustrer med R-pakken:
# devtools::install_github("martigso/NorSentLex")
# library(NorSentLex)
# Ordbøker i fullform
names(nor_fullform_sent)
## [1] "negative" "positive"
# Ordbøker for lemma med PoS-tags
names(nor_lemma_sent)
## [1] "lemma_adj_negative" "lemma_adj_positive" "lemma_noun_negative"
## [4] "lemma_noun_positive" "lemma_padj_negative" "lemma_padj_positive"
## [7] "lemma_verb_negative" "lemma_verb_positive"
Hvis vi vil se på, for eksempel, noen positive ord i fullform, kan vi gå inn i listen nor_fullform_sent og listeelementet som heter $positive:
nor_fullform_sent$positive %>% head()
## [1] "absolutt" "absolutta" "absolutte" "absoluttene" "absolutter"
## [6] "absoluttet"
nor_fullform_sent$positive %>% tail()
## [1] "ønsket" "ønskete" "ønskt" "ønskte"
## [5] "øyeblikkelig" "øyeblikkelige"
nor_fullform_sent$positive %>% sample(., 6)
## [1] "lett" "kjæresten" "sympatisør" "underbart" "tilrå"
## [6] "dufte"
Det er ikke nødvendigvis alt som gir mening som positive og negative ord, med mindre man har i bakhodet at dette er basert på kundeanmeldelser. Så vær varsom!
Om vi videre vil bruke den lemmatiserte ordboken, kan vi også trekke dette ut enkelt fra de forskjellige elementene i nor_lemma_sent. Si at vi skal bruke bare positive substantiv:
nor_lemma_sent$lemma_noun_positive %>% sample(., 6)
## [1] "skarpsinn" "engel" "jubilant" "fortjeneste" "enighet"
## [6] "forsiktighet"
Nå når vi vet hvordan vi finner ordboken, gjenstår å lære hvordan vi bruker den. La oss bruke fullformord fra No.4-albumet data-mappen (no4.rda) som eksempel. Først splitter vi opp teksten i ord (tokens):
library(tidytext)
load("./data/no4.rda")
no4 <- no4 %>%
group_by(titler) %>%
unnest_tokens(ord, tekst)
Så kryss-refererer vi hvert ord med de positive og negative fullformordene i ordboken:
no4$pos_sent <- ifelse(no4$ord %in% nor_fullform_sent$positive, 1, 0)
no4$neg_sent <- ifelse(no4$ord %in% nor_fullform_sent$negative, 1, 0)
table(no4$pos_sent,
no4$neg_sent,
dnn = c("positiv", "negativ"))
## negativ
## positiv 0 1
## 0 2062 117
## 1 217 1
Som vi ser, er det faktisk noen flere negative ord enn positive i albument. Men overvekten av ord er nøytrale (0 på begge). Vi kan også summere opp sentiment over sangene, og se om det er noe forskjell i sentiment mellom dem:
no4_sent <- no4 %>%
group_by(titler) %>%
summarize(pos_sent = mean(pos_sent),
neg_sent = mean(neg_sent)) %>%
mutate(sent = pos_sent - neg_sent)
no4_sent
## # A tibble: 12 × 4
## titler pos_sent neg_sent sent
## <chr> <dbl> <dbl> <dbl>
## 1 Alt vi ikke er 0.100 0.0502 0.0502
## 2 Du trenger ikke å bli stor 0.0537 0.0604 -0.00671
## 3 En av de levende 0.0819 0.0395 0.0424
## 4 Feil sted 0.0374 0.0561 -0.0187
## 5 Hele livet (Ft. Fredrik Høyer) 0.0421 0.0383 0.00383
## 6 Hjemme hos meg 0.0853 0.0155 0.0698
## 7 Hold deg fast 0.147 0.0333 0.113
## 8 Hvilket vi 0.0337 0.0506 -0.0169
## 9 Parentes 0.0563 0.0423 0.0141
## 10 Regndanse i skinnjakke (Ft. Fredrik Høyer) 0.0254 0.00847 0.0169
## 11 Så lenge vi finnes 0.266 0.131 0.135
## 12 Våre beste år 0.115 0.0513 0.0641
Ikke alverden forskjell, men noen sanger er med positive enn negative og motsatt. La oss visualisere:
no4_sent %>%
mutate(neg_sent = neg_sent * -1) %>%
ggplot(., aes(x = str_c(sprintf("%02d", 1:12),
". ",
str_sub(titler, 1, 7),
"[...]"))) +
geom_point(aes(y = neg_sent, color = "Negativ")) +
geom_point(aes(y = pos_sent, color = "Positiv")) +
geom_point(aes(y = sent, color = "Snitt")) +
geom_linerange(aes(ymin = neg_sent, ymax = pos_sent), color = "gray40") +
scale_color_manual(values = c("red", "cyan", "gray70")) +
labs(x = NULL, y = "Sentiment", color = NULL) +
ggdark::dark_theme_minimal() +
theme(axis.text.x = element_text(angle = 90, vjust = .25, hjust = 0))